// ==UserScript==
// @version	0.5: YUI-ext tabs with fixed position and nicer styles
// @version	0.4: Configuration now stored in a tidy JSON blob "config".
// @version	0.3: Thickbox replaced with crude YUI tab panel base. Temporarily turned off config restoring.
// @version	0.2: Nicer thickbox css that doesn't break margins of others. Added facet browsing and field removal. Dropped Google sites from default inclusion (gmail broke). Should work without Firebug installed too now.
// @version	0.1: Already works, a little, sort of.
// @name	Exhibit self-injector thickbox
// @namespace	http://code.google.com/p/ecmanaut/
// @description	Adds Exhibit DIY functionality to any web page. Just add XPath.
// @include	http://*
// @exclude	http://mail.google.com/*
// @exclude	http://docs.google.com/*
// ==/UserScript==
// $Id: exhibit-self-injector-thickbox.user.js 22 2007-02-21 00:48:21Z singpolyma $
var debug = 0; // for hacking only

var yuiext_0_40_css = 'http://www.jackslocum.com/deploy/yui-ext/resources/css/yui-ext-css-1228.php';

var panels = {
  scraper: { title:'Configuration' },
  exhibit: { title:'Exhibit' },
   facets: { title:'Facets' }
};

var looks = {
  modal: true,
  width: innerWidth - 100,
  height: innerHeight - 100,
  shadow: true,
  center: {
    autoScroll: true,
    closeOnTab: true,
    tabPosition: 'top',
    alwaysShowTabs: true
  },
  east: {
    split: true,
    animate: true,
    titlebar: true,
    autoScroll: true,
    collapsible: true,
    initialSize: 200
  }
};

var jscripts = 'http://jscripts.ning.com/ids/' + ([
  3575509, // YUI-util, http://us.js2.yimg.com/us.js.yimg.com/lib/common/utils/2/utilities_2.1.0.js
  // 3575689, // YUI-Ext Core Classes
  // 3575812, // Element
  // 3575847, // CompositeElement
  // 3575851, // DomHelper
  // 3575871, // EventManager
  // 3576130, // Animator Core
  // 3576230, // Layout State Manager, Border Layout Core, Button, Resizable, TabPanel, SplitBar, MixedCollection
  // 3576408 // BasicDialog
  3581625 // all of the above, plus State
  //3581708 // All of YUI-ext
].join('+'));

var exhibit_api = 'http://static.simile.mit.edu/exhibit/api/exhibit-api.js';
var yui_ext_api = jscripts + '?minify&callback=exposed.onload';

// Index the scraping template configuration by url, minus query portion:
var key = location.host + location.pathname;

var loaded, button, css;
var Ext, getEl, Exhibit;

var dialog = (function() {
  function css_on() { css.disabled = false; }
  function css_off() { css.disabled = true; }
  var dialog, layout, showBtn, center, east, view;
  return {
    init : function() {
      button.href = '#';
      button.innerHTML = '[Configure]';
      this.showBtn = showBtn = getEl( button );
      this.showBtn.on( 'click', this.show, this, true );

      this.dialog = dialog = new Ext.LayoutDialog( 'ExCfg-pane', looks );
      dialog.addKeyListener( 27, dialog.hide, dialog );

      var c = 'center', e = 'east';
      layout = dialog.getLayout();
      dialog.beginUpdate();
      layout.add( c, new Ext.ContentPanel('scraper', panels.scraper) );
      layout.add( c, new Ext.ContentPanel('exhibit', panels.exhibit) );
      layout.add( e, new Ext.ContentPanel('facets',  panels.facets) );
      center = layout.getRegion( c );
      east = layout.getRegion( e );
      view = center.getPanel('exhibit');
      center.showPanel( 'scraper' );
      east.hide();
      view.on( 'activate', east.show, east, true );
      view.on( 'deactivate', east.hide, east, true  );
      dialog.on( 'beforeshow', css_on );
      dialog.on( 'beforehide', function(){ setTimeout( css_off, 1e3 ); } );
      dialog.endUpdate();
    }, // init()

    hide : function() {
      dialog.hide();
    },

    focus : function( panel ) {
      center.showPanel( panel );
    },

    show : function( e ) {
      if( e ) { e.preventDefault(); e.stopPropagation(); }
      if( !dialog ) this.init();
      dialog.show( showBtn.dom );
    }
  };
})();

var exposed = unsafeWindow.exposed = {
  dialog:dialog, // for hacking only
  init: init,
  onload: onload,
  render_exhibit: render_exhibit,
  add_field: add_field,
};

function load_config( key ) {
  log('load_config');
  var configs = eval( GM_getValue( 'config', '({})' ) );
  return key ? configs[key] : configs;
}

function save_config( key, form ) {
  log('save_config');
  var configs = load_config();
  configs[key] = { scrape:read_config( form ) };
  GM_setValue( 'config', configs.toSource() );
}

function read_config( form ) {
  function get( id ) {
    var node = form.elements.namedItem( 'excfg-'+id );
    return node && node.value;
  }
  log( 'read_config', form );
  var config = { root:get('root'), rows:get('rows') };
  var fields = [], field = {}, i;
  var max = $X( '(.//input[starts-with(@id,"excfg-name")])[last()]', form ).id;
  max = parseInt( max.substr( 10 ), 10 );
  log( max );
  for( i = 0; i <= max; i++ ) {
    field.name = get('name'+i); if( null == field.name ) continue;
    field.path = get('path'+i);
    fields.push( field );
    field = {};
  }
  config.fields = fields;
  log( '=>', config );
  return config;
}

function inject_ui() {
  log('inject_ui');
  var field;
  var config = load_config( key ); // load scraper config from a prior visit
  var scrape = config ? config.scrape : { // or pick a cruddy default template:
    root:'//table[@class="xf"]',
    rows:'tbody/tr',
    fields:[{ name:'label',	path:'td[last()]/text()'},
	    { name:'host',	path:'td/text()' },
	    { name:'status',	path:'td[last()-1]/a/img/@alt' }/*,
	    { name:'',  	path:'' }*/ ]
  };

  // The config interface HTML bits:
  inject_after( <div id="ExCfg" style="display:none;"><div id="ExCfg-pane">
<div class="ydlg-hd">Exhibit self-injector</div>
<div class="ydlg-bd">
  <div class="ydlg-tab" title="Scraper" id="scraper">
    <!-- Nested inner-tab to safely add padding -->
    <div class="inner-tab">

<form id="ExCfg-form"><table>
<tr><th colspan="2"><label for="excfg-root">Scrape root node:</label></th>
<td>
  <input type="text" class="x" id="excfg-root" title="XPath to root"
	 value={scrape.root}/>
</td></tr>

<tr><th colspan="2"><label for="excfg-rows">Row selector:</label></th>
<td>
  <input type="text" class="x" id="excfg-rows" title="XPath to row"
	 value={scrape.rows}/>
</td></tr>

<tr><th>Fields:</th>
<th>
  <input type="text" class="n" id="excfg-name0" title="Attribute name"
	 value="label" readonly="readonly"/>:</th>
<td>
  <input type="text" class="x" id="excfg-path0" title="XPath to value"
	 value={scrape.fields[0].path}/></td>
</tr>

<tr id="excfg-add-before"><th colspan="3">
  <input type="button" id="excfg-add-field" value="Add Another Field"
	 onclick="exposed.add_field();"/>
  <input type="button" id="excfg-scrape-page" value="Scrape Page"
	 onclick="exposed.render_exhibit( this.form );"/>
</th></tr></table></form>

    </div><!-- inner-tab -->
  </div><!-- ydlg-tab (Scraper) -->
  <div class="ydlg-tab" title="Exhibit" id="exhibit">
    <div class="inner-tab">
      <div id="exhibit-control-panel"/>
      <div id="exhibit-view-panel"/>
    </div><!-- inner-tab -->
  </div><!-- ydlg-tab (Exhibit) -->

  <div class="ylayout-inactive-content" title="Facets" id="facets">
    <div id="exhibit-browse-panel"/>
  </div><!-- ylayout-inactive-content (Facets) -->
</div><!-- ydlg-bd -->
</div><!-- ExCfg-pane, ExCfg --></div> );

  for( var i=1; field = scrape.fields[i]; i++ ) {
    add_field( field );
  }
  $('ExCfg').innerHTML = $('ExCfg').innerHTML; // hack: activate event handlers
}

invoke_script();

function invoke_script() {
  log('invoke_script');
  if(parent != window) return;
  var url = 'javascript:exposed.init()', config_id = 'exhibit-configure-link';
  inject_after( <a href={url} id={config_id} title="Exhibit configuration" style="position:fixed;z-index:1;bottom:0px;right:0px;font-size:12pt;padding:5px;background-color:#333;opacity:0.8;color:black;text-decoration:none;">[Configure]</a> );
  button = $( config_id );
}

function add_css( css ) {
  var id = 'exhibit-autoinjector';
  var url = css.match( /^http/i );
  var style = $( id );
  if( !style ) {
    style = document.createElement( url ? 'link' : 'style' );
    if( url ) style.rel = 'stylesheet';
    style.type = 'text/css';
    style.id = id;
  }
  if( url )
    style.href = css;
  else
    style.innerHTML = css;
  if( !style.parentNode )
    $X( '//head' ).appendChild( style );
  return style;
}

function init() {
  log('init');
  if( loaded ) {
    show( 'ExCfg' );
    exposed.dialog.show();
  } else {
    button.innerHTML = '[<i>Loading...</i>]';
    inject_ui();
    add_script( exhibit_api );
    add_script( yui_ext_api );
    exposed.css = css = add_css( yuiext_0_40_css );
  }
}

function onload() {
  log('onload');
  Ext = unsafeWindow.YAHOO.ext;
  getEl = unsafeWindow.getEl;
  loaded = 1;
  init();
}

function remove_node( node ) {
  node.parentNode.removeChild();
}

function render_exhibit( form ) {
  log('render_exhibit');
  Exhibit = unsafeWindow.Exhibit;
  var data = scrape_page( form );
  log( "Scraped data: %o", data );
  save_config( key, form );
//  SimileAjax.History.historyFile = "...wherever/we/want/it/to/be.html";
  unsafeWindow.SimileAjax.History.enabled = false;
  exposed.exhibit = Exhibit.create( data, 'Item' );
  dialog.focus('exhibit');
}

function scrape_page( form ) {
  log( 'scrape_page' );
  var config = read_config( form );
  var root = $X( config.root );
  var rows = $x( config.rows, root );
  var fields = config.fields;
  var parsed = { items:[] }, names = [];
  for( var r = 0, row; row = rows[r]; r++ ) {
    // log( 'row ', r, ': ', row );
    var attr = {}, field;
    for( var c = 0; field = fields[c]; c++ ) {
      var value = $X( field.path, row );
      log( field.name, field.path, value );
      if( typeof value == 'object' ) // a node
	attr[field.name] = value.nodeValue;
      else
	attr[field.name] = value;
      // log( field.name, ' [path: ', field.path, ']: ', attr[field.name] );

      if( r == 0 ) // aggregate all field names (once only; first outer loop)
	names.push( field.name );
    }
    parsed.items.push( attr );
  }

  $( 'exhibit-browse-panel' ).setAttribute( 'facets', '.'+names.join(', .') );
  return parsed;
}

function log() {
  if( !("console" in unsafeWindow ) || !debug ) return;
  var args = Array.prototype.slice.apply( arguments );
  unsafeWindow.console.log.apply( this, args );
}

//! Add another row at the end of the config pane.
//! @param values
//!   Optional { name:field-label, path:xpath-expression } specification.
function add_field( values ) {
  log( 'add_field', values );
  values = values || { name:'', path:'' };
  var remove_row_button = '<input type="button" value="Remove" ' +
' onclick="p=this.parentNode.parentNode;p.parentNode.removeChild(p);" />:';
  var trs = $( 'ExCfg-form' ).getElementsByTagName( 'tr' );
  var next = trs[trs.length-1];
  var prev = trs[trs.length-2];
  var tr = prev.cloneNode( true );
  tr.getElementsByTagName( 'th' )[0].innerHTML = remove_row_button;
  var cols = tr.getElementsByTagName( 'input' );
  var name = cols[1], data = cols[2];
  log( name, data );
  var id = 1 + parseInt( name.id.replace( /\D+/g, '' ), 10 );
  name.id = 'excfg-name' + id; name.setAttribute( 'value', values.name );
  data.id = 'excfg-path' + id; data.setAttribute( 'value', values.path );
  name.removeAttribute( 'readonly' );
  next.parentNode.insertBefore( tr, next );
  name.focus();
}

function add_script( url ) {
  //var script = <script src={url} type="text/javascript"/>;
  var script = document.createElement( 'script' ); script.src = url;
  var head = $X( '//head[1]' );
  head.appendChild( script );
  //inject_after( script, head );
}

function inject_after( xml, targetNode ) {
  return append_to( xml, targetNode||document.body );
  var node = (new DOMParser).parseFromString( xml.toXMLString(),
					      'text/xml' ).firstChild;
  (targetNode||document.body).appendChild( node );
}

function append_to( e4x, node, doc ) {
  return node.appendChild( import_node( e4x, doc || node.ownerDocument ) );
}

function import_node( e4x, doc ) {
  var me = import_node, xhtml, domTree, import_me;
  me.Const = me.Const || { mimeType: 'text/xml' };
  me.Static = me.Static || {};
  me.Static.parser = me.Static.parser || new DOMParser;
  xhtml = <testing xmlns="http://www.w3.org/1999/xhtml" />;
  xhtml.test = e4x;
  domTree = me.Static.parser.parseFromString( xhtml.toXMLString(),
           me.Const.mimeType );
  import_me = domTree.documentElement.firstChild;
  while( import_me && import_me.nodeType != 1 )
    import_me = import_me.nextSibling;
  if( !doc ) doc = document;
  return import_me ? doc.importNode( import_me, true ) : null;
}

function show( id ) { $( id ).style.display = 'block'; }

function $( id ) { return document.getElementById( id ); }

function $X( xpath, root ) {
  var got = $x( xpath, root );
  return got instanceof Array ? got[0] : got;
}

function $x( xpath, root ) {
  try {
  var doc = root ? root.evaluate ? root : root.ownerDocument : document;
  var got = doc.evaluate( xpath, root||doc, null, 0, null ), next, result = [];
  switch( got.resultType )
  {
    case got.STRING_TYPE:
      return got.stringValue;
    case got.NUMBER_TYPE:
      return got.numberValue;
    case got.BOOLEAN_TYPE:
      return got.booleanValue;
    default:
      while( next = got.iterateNext() )
	result.push( next );
      return result;
  }
  } catch( e ) {
    unsafeWindow.console && unsafeWindow.console.trace();
    log( e );
  }
}

function node_to_e4x( node ) {
  return new XML( (new XMLSerializer).serializeToString( node ) );
}

function foreach( xpath, cb, root ) {
  var results = $x(xpath, root), node, i;
  for( i = 0; node = results[i]; i++ )
    cb( node, i );
}
